#include <GameEngine/Graphics/Texture/texture2d.h>

#include <GameEngine/Graphics/openGL.h>
#include <GameEngine/Core/Memory/memory_manager.h>

#include <QtGui/QImage>
#pragma warning( push )
#pragma warning( disable : 4312 )
#include <QtOpenGL/QGLContext>
#pragma warning( pop )

#include <iostream>



#ifdef GAMEENGINE_NAMESPACES
namespace GameEngine {
#endif

GAMEENGINE_IMPLEMENT_RTTI(Texture2d)


//! Default constructor
Texture2d::Texture2d() : id_(0), GraphicResource() {
	create_id();
}

//! Copy constructor
/*!
 @param rhs The texture to copy
 */
Texture2d::Texture2d(const Texture2d& rhs) : id_(rhs.id_), GraphicResource() {
	if ( id_ ) ResourceManager<ID,CONTEXTID>::instance()->reference_resource(*this);
}

//! Destructor
Texture2d::~Texture2d() {
	clean_id();
}

//! Assignment operator
/*!
 @param rhs The texture to copy
 */
Texture2d& Texture2d::operator=(const Texture2d& rhs) {
	if ( &rhs != this ) {
		ResourceManager<ID,CONTEXTID>* manager = (ResourceManager<ID,CONTEXTID>*)ResourceManager<ID,CONTEXTID>::instance();
		if ( id_ ) manager->unreference_resource(*this);
		id_ = rhs.id_;
		if ( id_ ) manager->reference_resource(*this);
	}
	return *this;
}

//! Check if the texture has been allocated
/*!
 @return True if the texture has been allocated, false otherwise
 */
bool Texture2d::initialized() const {
	return ( id_ != 0 );
}

//! Try to bind the texture
/*!
 @param enable_texturing If set to true, the two-dimensional texturing is enabled. Enabling this texturing
						 type should only be done once for every draw operation.
 @return True if texture could be bound, false otherwise
 */
bool Texture2d::bind_texture(bool enable_texturing) const {
	// Enable texturing if required
	if ( enable_texturing == true ) {
		glEnable(GL_TEXTURE_2D);
	}

	// Check id
	if ( id_ == 0 ) return false;

	// Bind texture id
	glBindTexture(GL_TEXTURE_2D, id_);
	return true;
}

//! Try to unbind the texture
/*!
 @param disable_texturing If set to true, the two-dimensional texturing is enabled. Disabling this texturing
						  type should only be done once after every draw operation.
 @return True if texture could be unbound, false otherwise
 */
bool Texture2d::unbind_texture(bool disable_texturing) const {
	// Disable texturing if required
	if ( disable_texturing == true ) {
		glEnable(GL_TEXTURE_2D);
	}

	// Unbind texture id
	glBindTexture(GL_TEXTURE_2D, 0);
	return true;
}

//! Set the texture application mode
/*!
 *  Texture should be bound for the parameters to take effect. Please note, however, that this
 *  function does NOT check if texture is bound or not.
 *
 @param mode The texture application mode to setup
 @return True if parameters could be setup, false otherwise
 */
bool Texture2d::set_parameters(Texture::ApplicationMode mode) {
	// Check id
	if ( id_ == 0 ) return false;
	if ( mode == Texture::UndefinedApplicationMode ) return false;

	// Set parameters
	glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, mode);
	return true;
}

//! Set the texture wrap mode
/*!
 *  Texture should be bound for the parameters to take effect. Please note, however, that this
 *  function does NOT check if texture is bound or not.
 *
 @param mode The texture wrap mode to setup
 @param wrap The texture wrap parameter to setup
 @return True if parameters could be setup, false otherwise
 */
bool Texture2d::set_parameters(Texture::WrapMode mode, WrapParameter wrap) {
	// Check id
	if ( id_ == 0 ) return false;

	// Set parameters
	if ( wrap == AllWrapParameter ) {
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, mode);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, mode);
	}
	else glTexParameteri(GL_TEXTURE_2D, wrap, mode);
	return true;
}

//! Set the texture magnifying filter
/*!
 *  Texture should be bound for the parameters to take effect. Please note, however, that this
 *  function does NOT check if texture is bound or not.
 *
 @param filter The texture magnifying filter to setup
 @return True if parameters could be setup, false otherwise
 */
bool Texture2d::set_parameters(Texture::MagnifyingFilter filter) {
	// Check id
	if ( id_ == 0 ) return false;
	if ( filter == Texture::UndefinedMagnifyingFilter ) return false;

	// Set parameters
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter);
	return true;
}

//! Set the texture minifying filter
/*!
 *  Texture should be bound for the parameters to take effect. Please note, however, that this
 *  function does NOT check if texture is bound or not.
 *
 @param filter The texture minifying filter to setup
 @return True if parameters could be setup, false otherwise
 */
bool Texture2d::set_parameters(Texture::MinifyingFilter filter) {
	// Check id
	if ( id_ == 0 ) return false;
	if ( filter == Texture::UndefinedMinifyingFilter ) return false;

	// Set parameters
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter);
	return true;
}

//! Set the texture mipmap mode
/*!
 *  Texture should be bound for the parameters to take effect. Please note, however, that this
 *  function does NOT check if texture is bound or not.
 *
 *  On most GPUs, this function should be called BEFORE texture is setup to take effect.
 *
 @param mipmap The texture mipmap mode to setup
 @return True if parameters could be setup, false otherwise
 */
bool Texture2d::set_parameters(Texture::MipmapMode mipmap) {
	// Check id
	if ( id_ == 0 ) return false;
	if ( mipmap == Texture::UndefinedMipmapMode ) return false;

	// Set parameters
	glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, mipmap);
	return true;
}

//! Return the OpenGL ID of the texture
/*!
 *  This ID should NOT be deleted using the glDeleteTextures() function, since the
 *  texture class takes care of its own memory and desallocates the texture when
 *  there are no more users on it.
 */
Texture2d::ID Texture2d::id() const {
	return id_;
}

//! Queries the internal format of the texture
/*!
 *  Texture should be bound for the parameters to take effect. Please note, however, that this
 *  function does NOT check if texture is bound or not.
 *
 @return The internal format of the texture
 */
Texture::TextureInternalFormat Texture2d::get_format() const {
	GLint format = -1;
	glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &format);
	if ( format < 0 ) return Texture::UndefinedInternalFormat;
	return (Texture::TextureInternalFormat)format;
}

//! Queries the application mode of the texture
/*!
 *  Texture should be bound for the parameters to take effect. Please note, however, that this
 *  function does NOT check if texture is bound or not.
 *
 @return The application mode of the texture
 */
Texture::ApplicationMode Texture2d::get_application_mode() const {
	GLint mode = -1;
	glGetTexEnviv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, &mode);
	if ( mode < 0 ) return Texture::UndefinedApplicationMode;
	return (Texture::ApplicationMode)mode;
}

//! Queries the magnifying filter of the texture
/*!
 *  Texture should be bound for the parameters to take effect. Please note, however, that this
 *  function does NOT check if texture is bound or not.
 *
 @return The magnifying filter of the texture
 */
Texture::MagnifyingFilter Texture2d::get_magnifying_filter() const {
	GLint filter = -1;
	glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, &filter);
	if ( filter < 0 ) return Texture::UndefinedMagnifyingFilter;
	return (Texture::MagnifyingFilter)filter;
}

//! Queries the minifying filter of the texture
/*!
 *  Texture should be bound for the parameters to take effect. Please note, however, that this
 *  function does NOT check if texture is bound or not.
 *
 @return The minifying filter of the texture
 */
Texture::MinifyingFilter Texture2d::get_minifying_filter() const {
	GLint filter = -1;
	glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, &filter);
	if ( filter < 0 ) return Texture::UndefinedMinifyingFilter;
	return (Texture::MinifyingFilter)filter;
}

//! Queries the mipmap mode of the texture
/*!
 *  Texture should be bound for the parameters to take effect. Please note, however, that this
 *  function does NOT check if texture is bound or not.
 *
 @return The mipmap mode of the texture
 */
Texture::MipmapMode Texture2d::get_mipmap_mode() const {
	GLint mode = -1;
	glGetTexParameteriv(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, &mode);
	if ( mode < 0 ) return Texture::UndefinedMipmapMode;
	return (Texture::MipmapMode)mode;
}

//! Queries the width of the texture
/*!
 *  Texture should be bound for the parameters to take effect. Please note, however, that this
 *  function does NOT check if texture is bound or not.
 *
 @return The width of the texture
 */
Texture::TextureSize Texture2d::get_width() const {
	GLint width = -1;
	glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &width);
	if ( width < 0 ) return 0;
	return (Texture::TextureSize)width;
}

//! Queries the height of the texture
/*!
 *  Texture should be bound for the parameters to take effect. Please note, however, that this
 *  function does NOT check if texture is bound or not.
 *
 @return The height of the texture
 */
Texture::TextureSize Texture2d::get_height() const {
	GLint height = -1;
	glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &height);
	if ( height < 0 ) return 0;
	return (Texture::TextureSize)height;
}

//! Fill the texture with the input color array
/*!
 *  Caution: make sure that the colors array has a size equal to width*height. If the size of the
 *  colors array is smaller than width*height, this function is likely to result in an application
 *  crash. If the size of the colors array is larger than width*height, all colors after the
 *  (width*height)-th one will be ignored.
 *
 *  Texture should be bound for the parameters to take effect. Please note, however, that this
 *  function does NOT check if texture is bound or not.
 *
 @param colors The array of colors that should be used to fill in the texture.
 @param width The width of the array of colors.
 @param height The height of the array of colors.
 @param format The internal format for the texture.
 @return True if texture was filled, false otherwise.
 */
bool Texture2d::set_texture(ColorRGB* colors, Texture::TextureSize width, Texture::TextureSize height, Texture::TextureInternalFormat format) {
	// Check input parameters
	if ( colors == nil ) return false;
	if ( width <= 0 ) return false;
	if ( height <= 0 ) return false;
	if ( format == Texture::UndefinedInternalFormat ) return false;

	// Setup texture
	glTexImage2D( GL_TEXTURE_2D, 0, format, width, height, 0, GL_RGB, GL_FLOAT, colors );
	return true;
}

//! Fill the texture with the input color array
/*!
 *  Caution: make sure that the colors array has a size equal to width*height. If the size of the
 *  colors array is smaller than width*height, this function is likely to result in an application
 *  crash. If the size of the colors array is larger than width*height, all colors after the
 *  (width*height)-th one will be ignored.
 *
 *  Texture should be bound for the parameters to take effect. Please note, however, that this
 *  function does NOT check if texture is bound or not.
 *
 @param colors The array of colors that should be used to fill in the texture.
 @param width The width of the array of colors.
 @param height The height of the array of colors.
 @param format The internal format for the texture.
 @return True if texture was filled, false otherwise.
 */
bool Texture2d::set_texture(ColorRGBA* colors, Texture::TextureSize width, Texture::TextureSize height, Texture::TextureInternalFormat format) {
	// Check input parameters
	if ( colors == nil ) return false;
	if ( width <= 0 ) return false;
	if ( height <= 0 ) return false;
	if ( format == Texture::UndefinedInternalFormat ) return false;

	// Setup texture
	glTexImage2D( GL_TEXTURE_2D, 0, format, width, height, 0, GL_RGBA, GL_FLOAT, colors );
	return true;
}

//! Fill the texture with the input image, specified in the Qt format
/*!
 *  Texture should be bound for the parameters to take effect. Please note, however, that this
 *  function does NOT check if texture is bound or not.
 *
 @param image The image that should be used to fill in the texture.
 @param format The internal format for the texture.
 @param use_transparency If not nil, receives true if texture has at least one non-opaque
                         texel (i.e., whose alpha channel is less than 1)
 @return True if texture was filled, false otherwise.
 */
bool Texture2d::set_texture(const QImage& image, Texture::TextureInternalFormat format, bool* use_transparency) {
	// Initialize the transparency use
	if ( use_transparency != nil ) *use_transparency = false;

	// Get image width and height
	int w = image.width();
	if ( w <= 0 ) return false;
	int h = image.height();
	if ( h <= 0 ) return false;

	// Fill color array
	ColorRGBA* colors = MemoryManager::instance()->allocate<ColorRGBA>(w*h);
	for ( int l = 0; l < h; ++l ) {
		for ( int k = 0; k < w; ++k ) {
			QRgb pixel = image.pixel(k,l);
			int i = k+l*w;
			colors[i].r() = (float)qRed(pixel) / 255.0;
			colors[i].g() = (float)qGreen(pixel) / 255.0;
			colors[i].b() = (float)qBlue(pixel) / 255.0;
			colors[i].a() = (float)qAlpha(pixel) / 255.0;

			// Check transparency
			if ( use_transparency != nil ) {
				if ( colors[i].a() < 1.0f ) {
					*use_transparency = true;
				}
			}
		}
	}

	// Fill texture with the image colors
	bool res = set_texture(colors, w, h, format);
	MemoryManager::instance()->desallocate(colors,w*h);
	return res;
}

//! Try to setup and bind the texture from a Qt image
/*!
 @param context The Qt context that allows to load the texture
 @param image The Qt image that should be loaded
 @return True if texture could be setup and bound, false otherwise
 */
bool Texture2d::set_texture(QGLContext* context, const QImage& image) {
	// Unreference previous ID
	ResourceManager<ID,CONTEXTID>* manager = (ResourceManager<ID,CONTEXTID>*)ResourceManager<ID,CONTEXTID>::instance();
	if ( id_ ) manager->unreference_resource(*this);

	// Setup new ID
	id_ = context->bindTexture(image);
	if ( id_ ) manager->reference_resource(*this);
	return ( id_ != 0 );
}

//! Clean the previous ID
void Texture2d::clean_id() {
	// Nothing to do if ID is not allocated
	if ( id_ == 0 ) return;

	// Unreference ID from the resource manager
	ResourceManager<ID,CONTEXTID>::instance()->unreference_resource(*this);

	// Clean ID member variable
	id_ = 0;
}

//! Allocate a new ID
void Texture2d::create_id() {
	// Clean previous ID, if any
	clean_id();

	// Generate a new texture ID
	glGenTextures(1, &id_);

	// Reference this ID
	ResourceManager<ID,CONTEXTID>::instance()->reference_resource(*this);
}

//! Desallocate the texture ID
void Texture2d::destroy_resource() const {
	Texture2dManager::instance()->remove_texture(*this);
	glDeleteTextures(1, &id_);
}

//! Return the texture context
Texture2d::CONTEXTID Texture2d::context() const {
	return rtti_id_;
}

//! Return the texture reference
Texture2d::ID Texture2d::reference() const {
	return id_;
}

//! Constructor from a texture ID
/*!
 @param id The texture ID
 */
Texture2d::Texture2d(ID id) : id_(id) {
	// Reference this ID
	ResourceManager<ID,CONTEXTID>::instance()->reference_resource(*this);
}

//=================================================================================

GAMEENGINE_IMPLEMENT_SINGLETON( Texture2dManager )

//! Default constructor
Texture2dManager::Texture2dManager() {}

//! Destructor
Texture2dManager::~Texture2dManager() {}

//! Try to add the texture to the manager
/*!
 @param texture The texture to add to the manager
 @param name The name of the texture
 @return True if texture could be added, false otherwise
 */
bool Texture2dManager::add_texture(const Texture2d& texture, const GEString& name) {
	// Check if texture is not already present in the manager
	if ( find_texture(name) == true ) return false;

	// Add the texture to the manager
	textures_.insert( std::pair<GEString,GraphicResource::ID>(name, texture.id()) );
	return true;
}

//! Try to remove the texture from the manager
/*!
 @param texture The texture to remove from the manager
 @return The number of textures that were removed from the manager
 */
int Texture2dManager::remove_texture(const Texture2d& texture) {
	// Iterate over the textures
	for ( std::map<GEString,GraphicResource::ID>::iterator it = textures_.begin(); it != textures_.end(); ++it ) {
		// Check the texture
		if ( it->second != texture.id() ) continue;

		// Delete the texture
		textures_.erase(it);
		return ( 1 + remove_texture(texture) );
	}

	// No such texture could be found
	return 0;
}

//! Try to remove the texture from the manager
/*!
 @param name The texture to remove from the manager
 @return True if the texture could be removed from the manager, false otherwise
 */
bool Texture2dManager::remove_texture(const GEString& name) {
	// Check if texture is not already present in the manager
	if ( find_texture(name) == false ) return false;

	// Remove the texture
	textures_.erase(name);
	return true;
}

//! Try to find the texture in the manager
/*!
 @param name The name of the texture to find
 @return True if texture could be found, false otherwise
 */
bool Texture2dManager::find_texture(const GEString& name) const {
	// Try to find the texture
	std::map<GEString,GraphicResource::ID>::const_iterator it = textures_.find(name);
	return ( it != textures_.end() );
}

//! Try to find the texture in the manager
/*!
 @param name The name of the texture to find
 @return The texture that could be found (id is null if the texture was not found)
 */
Texture2d Texture2dManager::get_texture(const GEString& name) const {
	// Try to find the texture
	std::map<GEString,GraphicResource::ID>::const_iterator it = textures_.find(name);
	GraphicResource::ID id = 0;
	if ( it != textures_.end() ) id = it->second;

	// Return the result
	return Texture2d(id);
}

//! Try to find the name with which the texture is known in the manager
/*!
 @param name Receives the name of the texture, if this texture is known by the manager
 @param texture The texture to find
 @return True if the texture could be found , false otherwise
 @note This function searches through the textures in linear time; it can therefore be
       very slow if a large number of textures is handled by the texture manager
 */
bool Texture2dManager::get_texture_name(GEString& name, const Texture2d& texture) const {
	// Iterate over the textures of the texture manager
	for ( std::map<GEString,GraphicResource::ID>::const_iterator it = textures_.begin(); it != textures_.end(); ++it ) {
		// Test current texture ID
		if ( it->second != texture.id() ) continue;

		// Return name
		name = it->first;
		return true;
	}
	return false;
}

#ifdef GAMEENGINE_NAMESPACES
} // namespace GameEngine
#endif
